import collections
import cStringIO
import inspect
import logging
import os
import pprint
import random
import re

from wmh import testing

import parser
import parser_test
import python

class TestCase(testing.TestCase):

  def setUp(self):
    super(TestCase, self).setUp()
    with open('./testdata/kernel.py', 'r') as fp:
      self._source = python.Source(None, 'kernel', 1, fp.read())

    self._module = python.Module.New('./testdata/kernel.py')
    self._class = self._module.classNamed('A')
    self._method = self._class.methodNamed('instance')
    self._method2 = self._class.methodNamed('Static')
    self._method3 = self._class.methodNamed('Meta')
    self._group = python.SourceGroup('def')
    self._group.add(self._method)
    self._group.add(self._method2)
    self._group.add(self._method3)
    self._metac = parser.Compiler(metal='oopl')
    self._ooplpy = self._metac.metalang().baselangNamed('python')

    # maps path to python.Module instances
    self._modules = {}

  def parseModule(self, filepath):
    modules = self._modules
    result = modules.get(filepath, None)
    if not result:
      result = python.Module.New(filepath)
      modules[filepath] = result
    return result

  def parseGivenClass(self, dotname):
    parts = dotname.split('.')
    clsname = parts.pop()
    path = './' + '/'.join(parts) + '.py'
    module = self.parseModule(path)
    return module.classNamed(clsname)

  def convertGivenClass(self, dotname):
    cls = self.parseGivenClass(dotname)
    output = python.Output()
    cls.toMeta(self._metac, self._ooplpy, output, indent='  ')
    return '\n'.join(output.lines()) + '\n'

  def parseGivenMethod(self, dotname):
    parts = dotname.split('.')
    methname = parts.pop()
    cls = self.parseGivenClass('.'.join(parts))
    return cls.methodNamed(methname)

  def convertGivenMethod(self, dotname):
    method = self.parseGivenMethod(dotname)
    output = python.Output(indent='    ')
    method.toMeta(self._metac, self._ooplpy, output)
    return '\n'.join(output.lines()) + '\n'


class SourceTest(TestCase):

  def test_Holder(self):
    # We do not explicitly write out the full placeholder name because that
    # will break the code if we use this code to convert this code to meta.
    # That is the reason python.Source.Holder() exists in the first place.
    self.assertEqual('<@' + 'STRING1' + '@>', python.Source.Holder(1))
    self.assertEqual('<@' + 'STRING27' + '@>', python.Source.Holder(27))

  def test___init__(self):
    source = self._source
    ph = source.placeholders()

    self.assertEqual(13, len(ph))
    
    self.assertMultiLineEqual(
      '"""Module documentation.\n'
      '\n'
      'The first docstr of the file.\n"""',
      ph[python.Source.Holder(1)])

    self.assertMultiLineEqual(
      '"""Class documentation.\n'
      '\n'
      '  Attributes:\n'
      '    strval: str\n'
      '      The string value.\n'
      '    intval: int\n'
      '      The int value.\n  """',
      ph[python.Source.Holder(2)])

    self.assertMultiLineEqual(
      '"""The line with leading/trailing whitespace (including newline) '
      'removed."""',
      ph[python.Source.Holder(3)])

    self.assertMultiLineEqual(
      '"""Provide string representation of this instance."""',
      ph[python.Source.Holder(4)])

    self.assertMultiLineEqual(
      '"""Class documentation.\n'
      '\n'
      '  Attributes:\n'
      '    data: dict\n  """',
      ph[python.Source.Holder(5)])

    method = self._class.methodNamed('Meta')
    self.assertMultiLineEqual('  @classmethod\n', method.source().preamble())

    self.assertEqual(
      {'source': {
        'body': 1544,
        'docstr': 1493,
        'end': 3698,
        'paren': 1481,
        'post': 3686,
        'pre': 1457,
        'scope': 1490,
        'start': 1474},
       'src': {
         'body': 1191,
         'docstr': 1180,
         'end': 2828,
         'paren': 1168,
         'post': 2816,
         'pre': 1144,
         'scope': 1177,
         'start': 1161}},
      self._class.source()._points)
    self.assertEqual(
      {'source': {
        'body': 1059,
        'docstr': 669,
        'end': 1402,
        'paren': 561,
        'post': 1215,
        'pre': 547,
        'scope': 664,
        'start': 547},
       'src': {
         'body': 640,
         'docstr': 629,
         'end': 983,
         'paren': 521,
         'post': 796,
         'pre': 507,
         'scope': 624,
         'start': 507}},
      self._method.source()._points)
    
  def test_name(self):
    self.assertEqual('kernel', self._module.source().name())
    self.assertEqual('A', self._class.source().name())
    self.assertEqual('instance', self._method.source().name())

  def test_parent(self):
    self.assertTrue(None is self._module.parent())
    self.assertTrue(self._module is self._class.parent())
    self.assertTrue(self._class is self._method.parent())

  def test_source(self):
    self.assertTrue(
        'Provide string representation of this instance.'
        in self._source.source())

  def test_src(self):
    # noop
    pass

  def test_start(self):
    # TODO(wmh): Use inspect.getsource() to obtain the first line for the
    # module, class and method, so that we don't need to keep changing these
    # values each time we add code to the test source code.
    self.assertEqual(1, self._module.source().start())
    self.assertEqual(77, self._class.source().start())
    self.assertEqual(107, self._method.source().start())

  def test_points(self):
    self.assertEqual(
      {'src': {
        'pre': 507, 'body': 640, 'end': 983, 'docstr': 629, 'start': 507,
        'paren': 521, 'scope': 624, 'post': 796},
       'source': {
         'body': 1059, 'pre': 547, 'end': 1402, 'docstr': 669, 'start': 547,
         'paren': 561, 'scope': 664, 'post': 1215}
       },
      self._method.source().points())

  def test__substr(self):
    # noop: tested by preamble(), postamble(), argamble() and body(). 
    pass

  def test_preamble(self):
    self.assertEqual('', self._module.source().preamble())
    self.assertEqual('# Preamble for A\n', self._class.source().preamble())
    self.assertEqual('', self._method.source().preamble())
    self.assertMultiLineEqual(
      '  # This comment is considered part of the preamble for Static() instead of\n'
      '  # being postamble for instance(), because the indentation matches that of\n'
      "  # Static and immediately preceeds the 'def'.\n"
      '  @staticmethod\n',
      self._method2.source().preamble())
    self.assertMultiLineEqual(
      '  @classmethod\n', self._method3.source().preamble())

  def test_postamble(self):
    self.assertEqual('', self._module.source().postamble())
    self.assertMultiLineEqual(
      '\nA.Meta(1)\n\n\n', self._class.source().postamble())
    self.assertMultiLineEqual(
      '\n'
      '  # This comment is considered part of the postamble for instance() because\n'
      '  # of the empty line between it and the next comment.  The empty line is\n'
      '  # also included in the postamble.\n'
      '\n',
      self._method.source().postamble())
    self.assertEqual('\n', self._method2.source().postamble())
    self.assertEqual('', self._method3.source().postamble())

  def test_argamble(self):
    self.assertEqual(
      '(object)', self._class.source().argamble())
    self.assertEqual(
      '(self, first_positional_argument, second_positional_argument,\n'
      '               first_keyword_argument=1)',
      self._method.source().argamble())
    self.assertEqual('(a)', self._method2.source().argamble())
    self.assertEqual('(cls, a)', self._method3.source().argamble())

  def test_docstr(self, sub=False):
    self.assertEqual(
      python.Source.Holder(2), self._method.source().docstr(sub=True))
    self.assertMultiLineEqual(
      '"""Static method.\n'
      '\n'
      'Args:\n'
      '  a: int\n'
      '    A simple int arg.\n'
      '"""',
      self._method2.source().docstr(sub=False))
    self.assertEqual(
      '"""Static method.\n'
      '\n'
      '    Args:\n'
      '      a: int\n'
      '        A simple int arg.\n'
      '    """',
      self._method2.source().docstr(sub=False, indent=True))

  def test_body(self):
    self.assertMultiLineEqual(
      '\n'
      'super(A, self).instance()\n'
      'if first_keyword_argument == 1:\n'
      '  return first_positional_argument\n'
      'else:\n'
      '  return second_positional_argument\n',
      self._method.source().body())
    self.assertMultiLineEqual(
      '\n'
      'super(A, self).instance()\n'
      'if first_keyword_argument == 1:\n'
      '  return first_positional_argument\n'
      'else:\n'
      '  return second_positional_argument\n',
      self._method.source().body(sub=True))
    self.assertMultiLineEqual(
      '\n'
      '    super(A, self).instance()\n'
      '    if first_keyword_argument == 1:\n'
      '      return first_positional_argument\n'
      '    else:\n'
      '      return second_positional_argument\n',
      self._method.source().body(sub=True, indent=True))

  def test_placeholders(self):
    self.assertEqual(13, len(self._module.source().placeholders().keys()))
    self.assertEqual(5, len(self._class.source().placeholders().keys()))
    self.assertEqual(1, len(self._method.source().placeholders().keys()))
    self.assertEqual(2, len(self._method2.source().placeholders().keys()))

  def test_split(self):
    if False:
      with open('parser.py', 'r') as fp:
        source = python.Source(None, 'parser', 1, fp.read())
      subs = source.split('class', '')
      #pprint.pprint(['%d: %s' % (sub.start(), sub._points) for sub in subs.values()])
      #print '#' * 70
      type_source = subs['Type']
      type_subs = type_source.split('def', '  ')
      pprint.pprint(['%d: %s' % (sub.start(), sub._points) for sub in type_subs.values()])
      return

    subs = self._source.split('class', '')
    self.assertEqual(
        [1, 13, 58, 77, 170, 174, 178],
        ([sub.start() for sub in subs.values()]))

  def test__identifySrcParts(self):
    full_source = python.Source(
      None, 'test', 1, '''  blah
      
  def f(a, b):
    """This is a test.
    To see how src parsing works.
    """
    return a * b

  this is postamble.
''')
    src = full_source.src()
    points = {'src': {}, 'source': {}}
    (close_paren_index, body_end_index, docstr, subindent, indent_len,
     single_line_def, sub_src, postamble
    ) = full_source._identifySrcParts(src, '  ', 14, len(src) - 1, points)
    source = python.Source(
      full_source, 'f', 3, full_source.source(), points=points)
    self.assertEqual(
      {'src': {
        'body': 44, 'end': 83, 'docstr': 33, 'scope': 28, 'paren': 21,
        'post': 62},
       'source': {}},
       points)
    self.assertEqual('<@STRING1@>', source.docstr(sub=True))
    self.assertEqual('\n    return a * b\n', source.body(sub=True))
    self.assertEqual('(a, b)', source.argamble(sub=True))
    self.assertEqual('\n  this is postamble.\n', source.postamble(sub=True))

  def test_strFor(self):
    self.assertMultiLineEqual(
      '"""\n'
      'this is a test to see how\n'
      'heredoc notation is parsed.\n'
      '"""',
      self._class.source().strFor(python.Source.Holder(4)))
    self.assertEqual(
      "'''Class method with no arg docs.'''",
      self._class.source().strFor(python.Source.Holder(5)))

  def test_resub(self):
    self.assertEqual(
      self._source.source(),
      self._source.resub(self._source.src()))

  def test_show(self):
    fp = cStringIO.StringIO()
    self._method2.source().show(fp=fp)
    self.maxDiff = None

    # TODO(wmh): Fix this when kernel.py is more stable.
    return
    self.assertMultiLineEqual(
      ' 113:   def Static(self, a):\n'
      ' 114:     """Static method.\n'
      ' 115: \n'
      ' 116:     Args:\n'
      ' 117:       a: int\n'
      ' 118:         A simple int arg.\n'
      ' 119:     """\n'
      ' 120:     # first some basic code\n'
      ' 121:     a *= 2\n'
      ' 122:     # Now a heredoc\n'
      ' 123:     val = """\n'
      ' 124: this is a test to see how\n'
      ' 125: heredoc notation is parsed.\n'
      ' 126: """\n'
      ' 127:     # And now some more basic code\n'
      ' 128:     print a\n'
      ' 129:     return val\n'
      ' 130: \n',
      fp.getvalue())


class SourceGroupTest(TestCase):

  def test___init__(self):
    self.assertEqual('def', self._group._keyword)

  def test_sources(self):
    self.assertEqual(
      ['instance', 'Static', 'Meta'], self._group.sources().keys())

  def test_add(self):
    self.assertEqual(3, len(self._group._sources))

  def test_find(self):
    self.assertEqual('instance', self._group.find('instance').name())
    self.assertEqual(None, self._group.find('no-such-source'))

  def test_names(self):
    self.assertEqual(
      ['instance', 'Static', 'Meta'], self._group.names())


class OutputTest(TestCase):

  def setUp(self):
    super(OutputTest, self).setUp()

    self._output = python.Output(indent='  ')
    self._output.addPrimary('class', 'Person')
    self.assertEqual(['  class Person'], self._output.lines())
    
    self._output2 = python.Output()
    self._output2.addPrimary('class', 'Person', features='abstract')
    self._output2.addSecondary('parent', 'Mammal')
    self._output2.addBlock(
      'comment', ['A comment', 'on multiple lines'], comments=True)
    self._output2.addBlock(
      'scope', ['def a(self): return self._a'])
    self._output2.addTerm('class', 'Person')

  def test_reset(self):
    self.assertEqual(2, self._output2._block_count)
    self._output2.reset()
    self.assertEqual(0, self._output2._block_count)

  def test_unblocked(self):
    self.assertTrue(self._output.unblocked())
    self.assertFalse(self._output2.unblocked())

  def test_clone(self):
    output = self._output2
    lines = []
    clone = output.clone(output=lines)
    self.assertEqual(output._newline_blocks, clone._newline_blocks)
    self.assertEqual(output._indent, clone._indent)
    self.assertEqual(output._termcode, clone._termcode)
    self.assertEqual(output._newline_features, clone._newline_features)
    self.assertTrue(lines is clone.lines())
    self.assertEqual(0, clone._block_count)
    self.assertEqual(0, clone._auto_idx)

  def test_lines(self):
    self.assertEqual(['  class Person'], self._output.lines())
    self.assertEqual(
      ['abstract',
       'class Person parent Mammal comment:',
       '  /# A comment',
       '  /# on multiple lines',
       'scope:',
       '  def a(self): return self._a',
       'end class Person;'],
      self._output2.lines())

  def test_addSecondary(self):
    output = self._output
    output.addSecondary('parent', 'Mammal')
    self.assertEqual(['  class Person parent Mammal'], output.lines())
    output.addSecondary('within', 'people', newline=True)
    self.assertEqual(
      ['  class Person parent Mammal', '  within people'], output.lines())

  def test_addBlock(self):
    output = self._output
    output.addBlock(
      'comment', ['A comment', 'on multiple lines'], comments=True)
    output.addBlock('scope', ['def a(self): return self._a'])
    self.assertEqual(
      ['  class Person comment:',
       '    /# A comment',
       '    /# on multiple lines',
       '  scope:',
       '    def a(self): return self._a'],
      output.lines())
        
  def test_addPrimary(self):
    output = self._output
    self.assertEqual(['  class Person'], output.lines())

  def test_addTerm(self):
    output = self._output
    output.addTerm('class', 'person')
    self.assertEqual(['  class Person;'], output.lines())
    # TODO(wmh): Add more tests here

  def test_newline(self):
    output = self._output
    output.newline()
    output.addSecondary('parent', 'Mammal')
    output.newline(count=2)
    self.assertEqual(
      ['  class Person', ' parent Mammal', '', ''], output.lines())

  def test_size(self):
    self.assertEqual(1, self._output.size())
    self.assertEqual(7, self._output2.size())


class EntityTest(TestCase):

  def setUp(self):
    super(EntityTest, self).setUp()
    path = './testdata/kernel.py'
    basename, source, test_source = python.Module._Load(path)
    self._block = python.Entity(source, test_sources=[test_source])

  def test___init__(self):
    self.assertEqual(1, self._block.source().start())

  def test_parseSource(self):
    entity = self._block
    classes, unclaimed = entity.parseSource(
      'module', python.Class, 'class', '', lambda name: name + 'Test',
      entity._source, test_sources=entity._test_sources)
    self.assertEqual(
      ['BaseClass', 'SubClass', 'A', 'B', 'C', 'FileParser'], classes.keys())

  def test_name(self):
    self.assertEqual('kernel', self._block.name())

  def test_parent(self):
    self.assertEqual(None, self._block.parent())

  def test_source(self):
    self.assertEqual(1, self._block.source().start())

  def test_previousSource(self):
    self.assertEqual(None, self._block.previousSource())

  def test_testSources(self):
    self.assertEqual(1, self._block.testSources()[0].start())

  def test_previousTestSources(self):
    self.assertEqual(None, self._block.previousTestSources())
    
  def test_start(self):
    self.assertEqual(1, self._block.start())

  def test_fullname(self):
    self.assertEqual('kernel', self._block.fullname())

  def test_splitText(self):
    self.assertEqual(
      ['this is', 'a simple test', 'for splitting'],
      self._block.splitText('this is\na simple test\nfor splitting\n'))
    self.assertEqual(
      ['this is', 'a simple test', 'for splitting'],
      self._block.splitText('\n\nthis is\na simple test\nfor splitting\n\n'))
    self.assertEqual(
      ['this is', 'a simple test', 'for splitting'],
      self._block.splitText(
        '   \n      \nthis is\na simple test\nfor splitting\n    \n   '))

  def test_nativeToMeta(self):
    output = python.Output()
    self._block.nativeToMeta(
      output, ['', '', 'some native text', '', 'on multiple lines', ''])
    self.assertEqual(
      ['',
       'native _1 scope:',
       '  some native text',
       '',
       '  on multiple lines',
       'end native;'],
      output.lines())


class ModuleTest(TestCase):

  def test_New(self):
    module = python.Module.New('./testdata/kernel.py')
    #module.summarize()
    #module2 = python.Module.New('./python.py')
    #module2.summarize()
    module3 = python.Module.New('./parser.py')
    #module3.summarize()
    classes = module3._classes
    class_count = len(classes)
    method_count = 0
    for name, clazz in classes.iteritems():
      method_count += len(clazz._methods)
    #print 'Parsed %d methods in %d classes' % (method_count, class_count)
    self.assertTrue(method_count > 320 and method_count < 400)
    self.assertEqual(50, class_count)

  def test__Load(self):
    path = './testdata/kernel.py'
    basename, source, test_source = python.Module._Load(path)
    self.assertEqual('kernel', basename)
    self.assertTrue('class BaseClass(object):' in source.source())
    self.assertTrue('class BaseClassTest(TestCase):' in test_source.source())

  def test___init__(self):
    self.assertEqual('./testdata/kernel.py', self._module._path)
    self.assertEqual(6, len(self._module._classes))

  def test_path(self):
    # noop
    pass

  def test_sourceText(self):
    self.assertTrue(
        'The first docstr of the file.' in self._module.sourceText())

  def test_classNamed(self):
    self.assertEqual('A', self._module.classNamed('A').name())
    self.assertEqual(None, self._module.classNamed('NotAClass'))

  def test_summarize(self):
    fp = cStringIO.StringIO()
    self._module.summarize(fp=fp)
    self.assertTrue(
      ': delimIs                                  |' in fp.getvalue())

  def test_toMeta(self):
    output = python.Output()
    self._module.toMeta(self._metac, self._ooplpy, output, indent='')

    filename = 'tmp/partial_parser2.meta'
    with open(filename, 'w') as fp:
      fp.write('\n'.join(output.lines()))
    print '\nDo something with ModuleTest.test_toMeta (%s)' % filename


class ClassTest(TestCase):

  def test___init__(self):
    self.assertEqual(
      ['__init__', 'delim', 'delimIs', 'data', 'dataIs', 'dataRef',
       'instance', 'Static', 'test', 'Meta'],
      self._class._methods.keys())

  def test_fieldMap(self):
    self.assertEqual(
      collections.OrderedDict(
        [('delim', {
          'field': '_delim',
          'value': 'delim',
          'type': 'str',
          'name':
          'delim',
          'comments': [
            'The delimiter between list elements.']}),
         ('data', {
           'field': '_data',
           'value': 'data',
           'type': 'list of str',
           'name': 'data',
           'comments': [
             'The elements of the list.']})]),
      self._class.fieldMap())

  def test_show(self):
    fp = cStringIO.StringIO()
    self._class.show(fp=fp)
    self.assertTrue('def Meta(cls, a):' in fp.getvalue())
    # TODO(wmh): More testing here.

  def test_summarize(self):
    fp = cStringIO.StringIO()
    self._class.summarize(fp=fp)
    self.assertTrue(
      ': data                                     | \n' in fp.getvalue())
    #print fp.getvalue()
    # TODO(wmh): Test full value of fp when kernel.py is more stable.

  def test_methodNamed(self):
    self.assertEqual('instance', self._class.methodNamed('instance').name())
    self.assertEqual(None, self._class.methodNamed('missing'))

  def test_methodsStartingWith(self):
    cls = self._module.classNamed('BaseClass')
    self.assertEqual(
      ['strval', 'strvalIs', 'strvalRef'],
      [m.name() for m in cls.methodsStartingWith('str')])

  def test_toMeta(self):
    output = python.Output()
    output.addPrimary('namespace', 'kernel.parser')

    classes_output = output.clone(delta_indent='  ')
    for clsname in (
        'parser.Logger', 'parser.Meta', 'parser.LiteralList', 'parser.Type'):
      cls = self.parseGivenClass(clsname)
      classes_output.reset()
      classes_output.newline()
      print 'Fix test_toMeta for %s' % clsname
      # cls.toMeta(self._metac, self._ooplpy, classes_output, indent='  ')

    output.addBlock('scope', classes_output.lines())
    output.newline()
    output.addTerm('namespace', 'kernel.parser')

    filename = 'tmp/partial_parser.meta'
    with open(filename, 'w') as fp:
      fp.write('\n'.join(output.lines()))

    print '\nDo something with ClassTest.test_toMeta (%s)' % filename
    #print self.convertGivenClass('parser.LiteralList')


class MethodTest(TestCase):

  def test___init__(self):
    self.assertEqual(None, self._module._parent)
    self.assertTrue(self._module is self._class._parent)

  def test_superInfo(self):
    self.assertEqual(
        {'text': 'super(A, self).instance()',
         'args': '',
         'inheritance': 'post_extend'},
      self._method.superInfo())
    self.assertEqual(None, self._method2.superInfo())
    method4 = self._class.methodNamed('test')
    self.assertEqual(
      {'text': 'super(A, self).test(a, b, c)',
       'args': 'a, b, c',
       'inheritance': 'pre_extend'},
      method4.superInfo())

  def test_level(self):
    self.assertEqual('instance', self._method.level())
    self.assertEqual('static', self._method2.level())
    self.assertEqual('meta', self._method3.level())

  def test_comments(self):
    self.assertEqual(
      {'args': {
        'first_positional_argument': {
          'type': 'int',
          'name': 'first_positional_argument',
          'comments': ['A simple int arg.']},
        'second_positional_argument': {
          'type': 'str',
          'name': 'second_positional_argument',
          'comments': [
            'A simple str arg. This docstr intentionally goes on and on so that we',
            'can test multi-line docs.']
          },
        'first_keyword_argument': {
          'type': 'int',
          'name': 'first_keyword_argument',
          'comments': [
            'A keyword arg. This docstr has',
            '  indented text', 'so we can verify it is preserved.']
          }
        },
       'comments': ['Instance method.']},
      self._method.comments())
    
    self.assertEqual(
      {'args': {
        'a': {
          'type': 'int',
          'name': 'a',
          'comments': ['A simple int arg.']
          }
        },
       'comments': ['Static method.']},
      self._method2.comments())

    self.assertEqual(
      {'comments': ['Class method with no arg docs.']},
      self._method3.comments())

  def test__parseComment(self):
    self.maxDiff = None
    self.assertEqual(
      {'args': {
        'first_positional_argument': {
          'type': 'int',
          'name': 'first_positional_argument',
          'comments': ['A simple int arg.']},
        'second_positional_argument': {
          'type': 'str',
          'name': 'second_positional_argument',
          'comments': [
            'A simple str arg. This docstr intentionally goes on and on so '
            'that we',
            'can test multi-line docs.']},
        'first_keyword_argument': {
          'type': 'int',
          'name': 'first_keyword_argument',
          'comments': [
            'A keyword arg. This docstr has',
            '  indented text',
            'so we can verify it is preserved.']}},
       'comments': ['Instance method.']},
       self._method._parseComment())

    class_ = self._module.classNamed('FileParser')
    method = class_.methodNamed('parseAttribute')
    self.assertEqual(
      {'args': {
        'construct': {
          'comments': [
            'The construct for which the attribute must be legal.'],
          'name': 'construct',
          'type': 'Construct'},
        'is_primary': {
          'comments': [
            'True if we are parsing a primary attribute, false if we are parsing',
            'a secondary attribute. There is some correlation between keyinfo and',
            'is_primary, but they are separate for added flexibility.'],
          'name': 'is_primary',
          'type': 'bool'},
        'keyinfo': {
          'comments': [
            'Usually, this is None, but if it is specified, it means the key portion',
            'of the attribute has already been parsed (and self._pos is past this',
            'key) so that we only need to parse the value.  If not specified, we are',
            'to also parse the attribute key itself.'],
          'name': 'keyinfo',
          'type': 'three-tuple or None'},
        'required_indent': {
          'comments': [
            'The required indentation level of the line that starts the attribute.',
            'An error is reported if this invariant is not maintained.'],
          'name': 'required_indent',
          'type': 'int'}},
       'comments': [
         'Parse an attribute key/value pair for the specified construct.',
         '',
         'Side Effects:',
         ' - self.pos() is advanced to the character after the attribute value.  If',
         "   the terminator (';') is encountered, self.pos() stays on that char.",
         '',
         'An attribute consists of:',
         ' - an attribute key or key abbrev.  Keys are always ids, but abbrevs can',
         '   be operators or ids or other words.',
         ' - an optional attribute parameter',
         ' - an attribute value:',
         '    - a literal int    i.e. 42            (arbitrary precision ... code uses appropriate datastructure to hold it)',
         '    - a literal float  i.e. 3.1415926535  (arbitrary precision ... code uses appropriate datastructure to hold it)',
         "    - a literal string i.e. 'this is a test'",
         '    - a literal list   i.e. List|<a|b|c>',
         "    - a word           i.e. '_39+.@$a'    (anything except whitespace)",
         "    - an identifier    i.e. 'age'         ([underalph][underalphanum]*)",
         '    - a block          i.e. colon followed by newline.',
         '',
         'Notes:',
         ' - in some situations, no attribute key is needed (for feature attributes,',
         '   and when the attribute value starts with some special character that',
         '   uniquely identifies it relative to all other attributes in the',
         '   construct).  Examples:',
         "    - the 'scope' attribute of most constructs is optional because its",
         "      value (a simple or complex block) starts with ':', and amongst all",
         "      block-valued attributes in all constructs, the 'scope' attribute",
         "      is marked as being optional-keyed ... if a ':<nl>' is seen when an",
         "      attribute key is expected, we know to assume 'scope'.  NOTE: We need",
         "      to decide how this interacts with attribute keys whose alias is ':'",
         '      (do we require key/values to appear on the same line???)',
         "    - the 'param' attribute of 'method' has an optional key because its",
         "      value, of type paramlist, starts with '(', and 'param' has been",
         "      marked as optional with '(' as its hint."],
       'raises': {
         'list': [
           {
             'comments': [],
             'first': 'If something',           
             'name': 'Error'},
           {
             'first': 'If something else that extends across multiple lines because',
             'comments': ['the comment is so long.'],
             'name': 'AnotherError'},           
           {
             'comments': [
               'A different variant of a multi-line exception comment that spans multiple',
               'lines so we can test it.'],
             'name': 'AThirdError'},
         ]
       },
       'returns': {
         'comments': [
           '[0] Attribute or None',
           '[1] int (the term code)',
           '  0 = not terminated',
           ' -1 = implicit (no terminator specified, but indentation or eof terminates)',
           "  1 = ';'",
           "  3 = 'end;'",
           "  7 = 'end <cons>;'",
           " 15 = 'end <cons> <id>;'",
           ''],
         'type': 'two-tuple'}},
      # The method has already been fully parsed, so _parseComments() has been
      # invoked and the result assigned to method._comments.
      method.comments())

    class_ = self._module.classNamed('FileParser')
    method = class_.methodNamed('test')
    #pprint.pprint(method.comments())

  def test__parseArgSpec(self):
    self.assertEqual(([], None, None, []), self._method._parseArgSpec(''))

    argspec = self._method._parseArgSpec(
      "a, b, *args, c=0, d='hello, world', e=f(1,g(2)), **kwds")
    self.assertEqual(
      (['a', 'b', 'c', 'd', 'e'], 'args', 'kwds',
       ['0', "'hello, world'", 'f(1,g(2))']),
      argspec)

  def test__parseArgInfo(self):
    self.assertEqual(
      [{'type': 'int',
        'name': 'first_positional_argument',
        'comments': ['A simple int arg.']},
       {'type': 'str',
        'name': 'second_positional_argument',
        'comments': [
          'A simple str arg. This docstr intentionally goes on and on so '
          'that we',
          'can test multi-line docs.']},
       {'default': '1',
        'type': 'int',
        'name': 'first_keyword_argument',
        'comments': [
          'A keyword arg. This docstr has',
          '  indented text',
          'so we can verify it is preserved.']}],
      self._method._parseArgInfo())
    # TODO(wmh): More testing here.

  def test_parseFieldInfo(self):
    method = self._class.methodNamed('__init__')
    self.assertEqual(
      [{'type': 'str',
        'name': 'delim',
        'field': '_delim',
        'comments': [
          'The delimiter between list elements.'],
        'value': 'delim',
       },
       {'type': 'list of str',
        'name': 'data',
        'field': '_data',
        'comments': [
          'The elements of the list.'],
        'value': 'data',
       }],
      method.parseFieldInfo())

  def test__convertDefaultValueToMeta(self):
    method = self._method
    for pyval, metaval in (
      ('True', 'true'),
      ('False', 'false'),
      ('None', 'null'),
      ('3.14', '3.14'),
      ('42', '42'),
      ('"apple"', '"apple"'),
      ("'apple'", "'apple'"),
      ('sys.stdout', '{#sys.stdout#}'),
    ):
      self.assertEqual(metaval, method._convertDefaultValueToMeta(pyval))

  def test_toMeta(self):
    output = python.Output()
    self._method.toMeta(self._metac, self._ooplpy, output)
    self.maxDiff = None
    self.assertMultiLineEqual(
      '\n'
      'post_extend\n'
      'method instance params:\n'
      '  var first_positional_argument : @int #:\n'
      '    /# A simple int arg.\n'
      '  var second_positional_argument : &str #:\n'
      '    /# A simple str arg. This docstr intentionally goes on and on so that we\n'
      '    /# can test multi-line docs.\n'
      '  var first_keyword_argument : @int = 1 #:\n'
      '    /# A keyword arg. This docstr has\n'
      '    /#   indented text\n'
      '    /# so we can verify it is preserved.\n'
      'comment:\n'
      '  /# Instance method.\n'
      'scope:\n'
      '  if first_keyword_argument == 1:\n'
      '    return first_positional_argument\n'
      '  else:\n'
      '    return second_positional_argument\n'
      'test:\n'
      '  /# Here in test.\n'
      '  a = 1\n'
      'end method instance;',
      '\n'.join(output.lines()))

    output = python.Output()
    self._method2.toMeta(self._metac, self._ooplpy, output)
    self.assertMultiLineEqual(
      '\n'
      'static\n'
      'method Static params:\n'
      '  var a : @int #:\n'
      '    /# A simple int arg.\n'
      'comment:\n'
      '  /# Static method.\n'
      'scope:\n'
      '  /# first some basic code\n'
      '  a *= 2\n'
      '  /# Now a heredoc\n'
      '  val = """\n'
      '     >|this is a test to see how\n'
      '     >|heredoc notation is parsed.\n'
      '     >|"""\n'
      '  /# And now some more basic code\n'
      '  print val\n'
      '  return a\n'
      'test:\n'
      '  self.assertEqual(4, kernel.A.Static(2))\n'
      'end method Static;',
      '\n'.join(output.lines()))

    self.assertMultiLineEqual(
      '\n'
      '    method rootDir comment:\n'
      '      /# Obtain the root directory for Meta.\n'
      '    scope:\n'
      "      rootdir = os.getenv('METAROOT')\n"
      '      if not rootdir or not IO.exists(rootdir):\n'
      "        raise meta.compiler.errors.Error('Failed to find METAROOT %s' % rootdir)\n"
      '      return rootdir\n'
      '    test:\n'
      '      self.assertEqual(\n'
      "        os.getenv('METAROOT'), self._meta.rootDir())\n"
      '    end method rootDir;\n',
      self.convertGivenMethod('parser.Meta.rootDir'))

    self.assertMultiLineEqual(
      '\n'
      '    meta\n'
      '    method StartsList params:\n'
      '      var val : &str #:\n'
      '        /# The value to analyze.\n'
      '    comment:\n'
      '      /# Determine if the given value represents a literal list.\n'  
      '    scope:\n'
      '      /# TODO(wmh): Need a much more efficient implementation - '
      'this is way\n'
      '      /# to expensive for something called very very often.\n'
      "      return val.startswith('List') and len(val) > 4 and "
      'not val[4].isalnum()\n'
      '    test:\n'
      "      self.assertTrue(parser.LiteralList.StartsList('List|'))\n"
      "      self.assertTrue(parser.LiteralList.StartsList('List<'))\n"
      "      self.assertTrue(parser.LiteralList.StartsList('List,'))\n"
      "      self.assertFalse(parser.LiteralList.StartsList('List'))\n"
      "      self.assertFalse(parser.LiteralList.StartsList('Lisf'))\n"
      '    end method StartsList;\n',
      self.convertGivenMethod('parser.LiteralList.StartsList'))

    #print self.convertGivenMethod('parser.FileParser.parseAttribute')


if __name__ == '__main__':
  logging.basicConfig(level=logging.INFO)
  testing.main()
